const vscode = window.acquireVsCodeApi?.() ?? { setState() {}, getState() {}, postMessage() {} }
const state = vscode.getState() || {}

class EventEmitter {
  constructor() {
    this._eventsMap = new Map()
  }
  on(eventName, callback) {
    let callbackList = this._eventsMap.get(eventName)
    if (!callbackList) {
      callbackList = new Set()
      this._eventsMap.set(eventName, callbackList)
    }
    callbackList.add(callback)
    return () => {
      return this.off(eventName, callback)
    }
  }
  off(eventName, callback = null) {
    if (!callback) {
      return this._eventsMap.delete(eventName)
    }
    const callbackList = this._eventsMap.get(eventName)
    if (callbackList) {
      const bool = callbackList.delete(callback)
      if (callbackList.size === 0) this._eventsMap.delete(eventName)
      return bool
    }
    return false
  }
  once(eventName, callback) {
    const offEvent = this.on(eventName, (data) => {
      offEvent()
      callback(data)
    })
    return offEvent
  }
  emit(eventName, data = null) {
    const callbackList = this._eventsMap.get(eventName)
    if (callbackList) {
      for (const callback of callbackList) {
        Promise.resolve(data).then(callback)
      }
    }
  }
}

class Messager extends EventEmitter {
  constructor(vscode) {
    super()

    this._id = 0
    this._vscode = vscode

    window.addEventListener('message', (res) => {
      console.debug('window.addEventListener.message', res.data)

      const e = res.data
      if (!e) return console.warn('no data', res)

      const { id, type, data, error } = e

      if (id) {
        this.emit(`send_event_${id}`, { error, data })
      } else if (type) {
        this.emit(`on_event_${type}`, { error, data })
      } else {
        console.warn('invalid data', res)
      }
    })
  }
  send(method, params) {
    const id = ++this._id
    const args = { id, method, params }
    return new Promise((resolve, reject) => {
      this.once(`send_event_${id}`, ({ error, data }) => {
        if (error) reject(error)
        else resolve(data)
      })
      console.debug('vscode.postMessage', args)
      this._vscode.postMessage(args)
    })
  }
  listen(type, callback) {
    return this.on(`on_event_${type}`, callback)
  }

  onChangedActiveFilePath(callback) {
    return this.listen('changed.activeFilePath', callback)
  }
  onChangedBranches(callback) {
    return this.listen('changed.branches', callback)
  }
  onActionClearAll(callback) {
    return this.listen('action.clearAll', callback)
  }
  async init(params, callback) {
    return this.send('init', params)
  }
  async queryFiles(params, callback) {
    return this.send('queryFiles', params)
  }
  async blameFile(params, callback) {
    return this.send('blameFile', params)
  }
}

/* 
request

init
queryFiles
blameFile


response

data.files
data.blameFile
changed.branches
changed.activeFilePath
action.clearAll
 */

const messager = new Messager(vscode)

const app = Vue.createApp({
  data: () => ({
    files: state.files || null,
    branches: state.branches || [],
    branch1: state.branch1 || '',
    branch2: state.branch2 || '',
    author: state.author || '',

    activeFilePath: '',
    lastActiveFilePath: state.lastActiveFilePath || '',

    filters: {
      filePath: state.filters?.filePath || '',
      author: state.filters?.author || '',
    },

    loading: false,
  }),
  computed: {
    queryDisabled() {
      return !this.branch1 || !this.branch2
    },
    authors() {
      const res = []
      const q = new Set()
      this.files?.forEach((v) => {
        v.authors?.forEach((v) => {
          if (!q.has(v.email)) {
            res.push(v)
            q.add(v.email)
          }
        })
      })
      return res
    },
    filterFilePathExp() {
      let { filePath } = this.filters
      filePath = filePath.trim()
      if (filePath) {
        try {
          return new RegExp(filePath)
        } catch {
          return null
        }
      }
      return null
    },
    showFiles() {
      let { files, filterFilePathExp, authorEmail } = this
      if (!files) return []
      if (authorEmail) {
        files = files.filter((v) => v.authors.some((v) => v.email === authorEmail))
      }
      if (filterFilePathExp) {
        files = files.filter((v) => filterFilePathExp.test(v.filePath))
      }
      return files
    },
    showBranches() {
      return this.branches.map((v) => ({
        value: v,
        title: v,
      }))
    },
    showAuthors() {
      return this.authors.map((v) => {
        const title = `${v.name} <${v.email}>`
        return {
          title,
          value: title,
        }
      })
    },
    authorEmail() {
      const m = this.filters.author.match(/^.*? <(.*)>$/)
      if (m) return m[1]
      return ''
    },
  },
  watch: {
    branches(branches) {
      state.branches = Vue.toRaw(branches)
      vscode.setState(state)
    },
    files(files) {
      state.files = Vue.toRaw(files)
      vscode.setState(state)
    },
    lastActiveFilePath(lastActiveFilePath) {
      state.lastActiveFilePath = Vue.toRaw(lastActiveFilePath)
      vscode.setState(state)
    },
    filters: {
      handler(filters) {
        state.filters = Vue.toRaw(filters)
        vscode.setState(state)
      },
      deep: true,
    },
  },
  async created() {
    messager.onActionClearAll(this.clearAll)
    messager.onChangedBranches(this.updateBranches)
    messager.onChangedActiveFilePath(this.updateActiveFilePath)
    this.debounceClickFile = debounce(this.clickFile)
    await this.init()
    await this.$nextTick()
    if (!this.activeFilePath) this.$refs.ul?.querySelector('li.hover')?.scrollIntoViewIfNeeded()
  },
  methods: {
    async clearAll() {
      state.branch1 = this.branch1 = ''
      state.branch2 = this.branch2 = ''
      state.author = this.author = ''
      state.files = this.files = null
      Object.keys(this.filters).forEach((k) => (this.filters[k] = ''))
      state.filters = this.filters
      vscode.setState(null)
    },
    updateBranches({ error, data }) {
      if (error) throw error
      this.branches = data
    },
    async updateActiveFilePath({ error, data }) {
      if (error) throw error

      clearTimeout(this._updateActiveFilePathTimer)
      if (data.filePath) {
        this.activeFilePath = this.lastActiveFilePath = data.filePath
      } else {
        this._updateActiveFilePathTimer = setTimeout(() => {
          this.activeFilePath = ''
        }, 300)
      }

      await this.$nextTick()
      const activeLi = this.$refs.ul?.querySelector('li.active')
      if (activeLi) activeLi.scrollIntoViewIfNeeded()
    },
    async init() {
      await messager.init()
    },
    async clickFile({ filePath, actionFlag, srcFilePath, authors }, { preview = false } = {}) {
      this.activeFilePath = filePath
      let { authorEmail } = this
      const selectCommitHashList = []
      authors?.forEach((v) => {
        if (!authorEmail || v.email === authorEmail) {
          v.commits?.forEach((v) => selectCommitHashList.push(v.hash))
        }
      })
      await messager.blameFile({
        commitIdStart: state.branch1,
        commitIdEnd: state.branch2,
        author: state.author,
        filePath,
        actionFlag,
        srcFilePath,
        preview,
        selectCommitHashList,
      })
    },
    async query() {
      this.files = null
      this.filters.filePath = ''
      this.filters.author = ''

      state.branch1 = Vue.toRaw(this.branch1)
      state.branch2 = Vue.toRaw(this.branch2)
      state.author = Vue.toRaw(this.author)
      vscode.setState(state)

      if (this.queryDisabled) return

      try {
        this.loading = true
        const files = await messager.queryFiles({
          commitIdStart: state.branch1,
          commitIdEnd: state.branch2,
          author: state.author,
        })
        this.files = files
      } finally {
        this.loading = false
      }
    },
  },
})
const HighlightText = Vue.defineComponent({
  name: 'HighlightText',
  props: ['exp', 'txt'],
  render() {
    const { exp, txt } = this
    if (!exp) return txt
    const m = txt.match(exp)
    if (!m) return txt
    const startIndex = m.index
    const t = m[0]
    const endIndex = startIndex + t.length
    return [
      txt.slice(0, startIndex),
      Vue.h('span', { class: 'highlight' }, txt.slice(startIndex, endIndex)),
      txt.slice(endIndex),
    ]
  },
})
const InputSelect = Vue.defineComponent({
  name: 'InputSelect',
  props: ['modelValue', 'options'],
  emits: ['update:modelValue'],
  data: () => ({
    oldValue: '',
    showed: false,
    optionIndex: null,
  }),
  computed: {
    changed() {
      return this.oldValue !== this.modelValue
    },
    showedOptions() {
      const { filterExp, options } = this
      if (!options) return []
      if (this.changed && filterExp) {
        return options.filter((v) => filterExp.test(v.value))
      }
      return options
    },
    filterExp() {
      const value = this.modelValue.trim()
      if (value) {
        try {
          return new RegExp(value)
        } catch {
          return null
        }
      }
      return null
    },
  },
  methods: {
    updateValue(v) {
      this.$emit('update:modelValue', v)
    },
    onInput(v) {
      this.updateValue(v.target.value)
      this.showOptions()
    },
    onFocus() {
      this.showOptions()
      this.$nextTick(() => {
        this.$el.querySelector('.InputSelect-item.active')?.scrollIntoViewIfNeeded()
      })
    },
    onBlur() {
      this._hideTimer = setTimeout(() => {
        this.hideOptions()
      }, 100)
    },
    onClick() {
      if (!this.showed) {
        this.showOptions()
        this.$nextTick(() => {
          this.$el.querySelector('.InputSelect-item.active')?.scrollIntoViewIfNeeded()
        })
      }
    },
    onKeydown(e) {
      if (e.ctrlKey || e.altKey) return
      switch (e.key) {
        case 'Escape':
          return this.hideOptions()
        case 'Enter':
          if (this.showed) {
            e.preventDefault()
            const v = this.showedOptions[this.optionIndex]
            if (v) this.clickOption(v.value)
          }
          return
        case 'ArrowDown':
          e.preventDefault()
          if (!this.showed) this.showOptions()
          if (this.optionIndex == null) this.optionIndex = 0
          else ++this.optionIndex
          this.optionIndex = this.optionIndex % this.showedOptions.length
          this.$nextTick(() => {
            this.$el
              .querySelector(`.InputSelect-item:nth-child(${this.optionIndex + 1})`)
              ?.scrollIntoViewIfNeeded()
          })
          return
        case 'ArrowUp':
          e.preventDefault()
          if (!this.showed) this.showOptions()
          const len = this.showedOptions.length
          if (this.optionIndex == null) this.optionIndex = len - 1
          else --this.optionIndex
          this.optionIndex = (this.optionIndex + len) % len
          this.$nextTick(() => {
            this.$el
              .querySelector(`.InputSelect-item:nth-child(${this.optionIndex + 1})`)
              ?.scrollIntoViewIfNeeded()
          })
          return
      }
    },
    onMousedown() {
      setTimeout(() => {
        clearTimeout(this._hideTimer)
      }, 0)
    },
    clickOption(v) {
      this.updateValue(v)
      this.hideOptions(true)
    },
    showOptions() {
      if (!this.showed) {
        const { modelValue } = this
        this.oldValue = modelValue
        this.optionIndex = this.showedOptions.findIndex((v) => v.value === modelValue)
        this.showed = true
      }
    },
    hideOptions(changed = false) {
      if (this.showed) {
        if (!changed) this.updateValue(this.oldValue)
        this.oldValue = ''
        this.showed = false
      }
    },
    clear() {
      this.clickOption('')
    },
  },
  render() {
    const { modelValue, oldValue, onMousedown, filterExp, changed, optionIndex, showed } = this
    return Vue.h('div', { class: 'InputSelect' }, [
      Vue.h('input', {
        value: modelValue,
        onClick: this.onClick,
        onInput: this.onInput,
        onFocus: this.onFocus,
        onBlur: this.onBlur,
        onKeydown: this.onKeydown,
      }),
      modelValue &&
        Vue.h('i', { class: 'InputSelect-clear codicon codicon-close', onClick: this.clear }),
      showed &&
        Vue.h(
          'div',
          { class: 'InputSelect-options' },
          this.showedOptions.map((v, i) =>
            Vue.h(
              'div',
              {
                key: v.value,
                class: [
                  'InputSelect-item',
                  {
                    active: oldValue === v.value,
                    selected: optionIndex === i,
                  },
                ],
                onMousedown,
                onClick: () => this.clickOption(v.value),
              },
              this.$slots.default?.({
                exp: changed ? filterExp : null,
                txt: v.title,
                item: v,
              }) ??
                Vue.h(HighlightText, {
                  exp: changed ? filterExp : null,
                  txt: v.title,
                })
            )
          )
        ),
    ])
  },
})
const vm = app
  .component('HighlightText', HighlightText)
  .component('InputSelect', InputSelect)
  .mount('#app')

function debounce(func, timeout = 0) {
  let timer
  let pro
  let resolve
  let reject
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(async () => {
      if (pro) {
        const _resolve = resolve
        const _reject = reject
        pro = resolve = reject = undefined
        try {
          _resolve(await func(...args))
        } catch (ex) {
          _reject(ex)
        }
      }
    }, timeout)
    if (!pro) {
      pro = new Promise((a, b) => {
        resolve = a
        reject = b
      })
    }
    return pro
  }
}
